package movee.snmp.client.snmp;

import lombok.extern.slf4j.Slf4j;
import org.snmp4j.CommunityTarget;
import org.snmp4j.PDU;
import org.snmp4j.Snmp;
import org.snmp4j.TransportMapping;
import org.snmp4j.event.ResponseEvent;
import org.snmp4j.mp.SnmpConstants;
import org.snmp4j.smi.OID;
import org.snmp4j.smi.OctetString;
import org.snmp4j.smi.UdpAddress;
import org.snmp4j.smi.VariableBinding;
import org.snmp4j.transport.DefaultUdpTransportMapping;
import org.snmp4j.util.DefaultPDUFactory;
import org.snmp4j.util.TreeEvent;
import org.snmp4j.util.TreeUtils;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * snmp简单客户端
 */
@Slf4j
public class SnmpClient implements AutoCloseable {

    public static final String OID_SYS_NAME = "1.3.6.1.2.1.1.5.0";
    public static final String OID_SYS_UPTIME = "1.3.6.1.2.1.1.3.0";
    private static final Integer DEFAULT_VERSION = SnmpConstants.version2c;
    private static final String DEFAULT_COMMUNITY = "public";
    private static final Integer DEFAULT_PORT = 161;
    private static final Integer RETRIES = 0;
    private static final Long TIMEOUT_MS = 1000L;

    private final String community;
    private final int version;
    private final int retries;
    private final long timeoutMs;
    private final Snmp snmp;

    public SnmpClient() throws IOException {
        this(DEFAULT_VERSION, DEFAULT_COMMUNITY);
    }

    public SnmpClient(int version, String community) throws IOException {
        this(version, community, RETRIES, TIMEOUT_MS);
    }

    public SnmpClient(int version, String community, int retries, long timeoutMs) throws IOException {
        this.version = version;
        this.community = community;
        this.retries = retries;
        this.timeoutMs = timeoutMs;

        TransportMapping<UdpAddress> transport = new DefaultUdpTransportMapping();
        snmp = new Snmp(transport);

        transport.listen();
    }

    public List<SnmpValue> snmpget(String ipAddr, List<String> oids) throws Exception {

        return snmpget(ipAddr, DEFAULT_PORT, oids);
    }

    public List<SnmpValue> snmpget(String ipAddr, int port, List<String> oids) throws Exception {

        List<SnmpValue> values = new ArrayList<>();

        CommunityTarget target = buildTarget(ipAddr, port);
        PDU pdu = buildPDU(oids);

        ResponseEvent responseEvent = snmp.get(pdu, target);
        if (responseEvent != null) {
            PDU responsePDU = responseEvent.getResponse();
            if (responsePDU != null) {
                if (responsePDU.getErrorStatus() == PDU.noError) {
                    List<? extends VariableBinding> bindings = responsePDU.getVariableBindings();
                    for (VariableBinding binding : bindings) {
                        SnmpValue value = SnmpValue.builder()
                                .oid(binding.getOid().toString())
                                .syntax(binding.getVariable().getSyntaxString())
                                .value(binding.getVariable().toString())
                                .build();
                        values.add(value);
                        log.debug("oid: {}, variable: {}, {}, {}, {}", binding.getOid().toString(),
                                binding.getVariable(),
                                binding.getSyntax(),
                                binding.getVariable().getSyntaxString(),
                                binding.getVariable().getSyntax());
                    }
                } else {
                    log.warn("snmpget responsePDU error. responsePDU: {}", responsePDU);
                }
            } else {
                log.error("snmpget responsePDU is null - error:{} peerAddress:{} source:{} request:{}",
                        responseEvent.getError(),
                        responseEvent.getPeerAddress(),
                        responseEvent.getSource(),
                        responseEvent.getRequest());
            }
        } else {
            log.error("snmpget timeout.");
        }

        return values;
    }

    public List<SnmpValue> snmpwalk(String ipAddr, String oid) {
        return snmpwalk(ipAddr, DEFAULT_PORT, oid);
    }

    public List<SnmpValue> snmpwalk(String ipAddr, int port, String oid) {

        List<SnmpValue> values = new ArrayList<>();

        TreeUtils treeUtils = new TreeUtils(snmp, new DefaultPDUFactory());
        CommunityTarget target = buildTarget(ipAddr, port);
        List<TreeEvent> events = treeUtils.getSubtree(target, new OID(oid));
        if (events != null) {
            for (TreeEvent event : events) {
                if (event.isError()) {
                    log.warn("Error: table OID [" + oid + "] " + event.getErrorMessage());
                    continue;
                }

                VariableBinding[] bindings = event.getVariableBindings();
                if (bindings != null) {
                    for (VariableBinding binding : bindings) {
                        SnmpValue value = SnmpValue.builder()
                                .oid(binding.getOid().toString())
                                .syntax(binding.getVariable().getSyntaxString())
                                .value(binding.getVariable().toString())
                                .build();
                        values.add(value);
                        log.debug("oid: {}, variable: {}, {}, {}, {}", binding.getOid().toString(),
                                binding.getVariable(),
                                binding.getSyntax(),
                                binding.getVariable().getSyntaxString(),
                                binding.getVariable().getSyntax());
                    }
                }
            }
        }

        return values;
    }

    private CommunityTarget buildTarget(String ipAddr, int port) {
        CommunityTarget target = new CommunityTarget();
        target.setCommunity(new OctetString(community));
        target.setVersion(version);
        target.setAddress(new UdpAddress(ipAddr + "/" + port));
        // 通信不成功时的重试次数
        target.setRetries(retries);
        // 超时时间
        target.setTimeout(timeoutMs);

        return target;
    }

    private PDU buildPDU(List<String> oids) {
        PDU pdu = new PDU();
        for (String oid : oids) {
            pdu.add(new VariableBinding(new OID(oid)));
        }
        return pdu;
    }

    @Override
    public void close() throws Exception {
        if (snmp != null) {
            snmp.close();
        }
    }

}
